package dslab.util;

import dslab.enums.Outcome;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.*;
import java.util.concurrent.Callable;

/**
 * DMTPClient that is used for outgoing TransferServer transfers
 * (usually in the form of a task for a SendQueueHandler)
 */
public class DMTPClient implements Callable<Outcome> {
    // 15 second timeout
    private static final int TIMEOUT = 15*1000;

    private final String componentId;
    private final Message message;
    private final boolean logging;
    private final String host;
    private final int port;

    /**
     * constructor for a DMTP client that performs no dynamic lookup and is instead loaded with a static
     * configuration
     *
     * @param componentId string representation of component
     * @param message message to transmit
     * @param host host to contact
     * @param port port to contact at
     * @param logging whether to output logging information
     */
    public DMTPClient(String componentId, Message message, String host, int port, boolean logging) {
        this.componentId = componentId;
        this.message = message;
        this.host = host;
        this.port = port;
        this.logging = logging;
    }

    @Override
    public Outcome call() {
        // notify user of what is happening
        System.out.printf(
            "@%s: processing message %s%n",
            componentId,
            message.toString()
        );

        // by default, outcome is a success
        Outcome outcome = Outcome.Success;

        // construct arrays describing DMTP exchange
        // from the perspective of a client
        String[] requests = {
            "ok DMTP",
            "ok",
            //String.format("ok %d", groupedRecipients.get(domain).size()),
            "ok",
            "ok",
            "ok",
            "ok",
            "ok",
            "ok",
            "ok bye"
        };
        String[] responses = {
            "begin",
            String.format("to %s", message.recipients),
            String.format("from %s", message.sender),
            String.format("subject %s", message.subject),
            String.format("data %s", message.data),
            message.hash == null ? null : String.format("hash %s", message.hash),
            "send",
            "quit"
        };

        // try-with-resources block for resource objects
        try (
            Socket client = new Socket(host, port);
            PrintWriter clientOut = new PrintWriter(client.getOutputStream(), true);
            BufferedReader clientIn = new BufferedReader(new InputStreamReader(client.getInputStream()))
        ) {
            // register timeout
            client.setSoTimeout(TIMEOUT);
            // follow specified protocol exchange
            for (int rounds = 0; rounds < responses.length; rounds++) {
                // skip sending fields that aren't set
                if (responses[rounds] == null)
                    continue;
                String line = clientIn.readLine();
                if (!line.contains(requests[rounds]))
                    throw new DMTPProtocolException(line);
                clientOut.println(responses[rounds]);
            }

            // notify user of successful transfer
            if (logging)
                System.out.printf("@%s: message delivered successfully%n", componentId);
        } catch (DMTPProtocolException | IOException exception) {
            // deal with various exception cases
            if (exception instanceof DMTPProtocolException) {
                if (logging)
                    System.err.printf("@%s: an error has occurred: server has responded unexpectedly %s:%d%n", componentId, host, port);
                outcome = Outcome.ProtocolError;
            } else if (exception instanceof SocketTimeoutException) {
                if (logging)
                    System.err.printf("@%s: an error has occurred: socket timed out %s:%d%n", componentId, host, port);
                outcome = Outcome.SocketTimeout;
            } else if (exception instanceof UnknownHostException) {
                if (logging)
                    System.err.printf("@%s: an error has occurred: cannot reach the host %s:%d%n", componentId, host, port);
                outcome = Outcome.UnknownHost;
            } else {
                // anything that falls under this category is an IOException
                if (logging) {
                    System.err.printf("@%s: an error has occurred: i/o error for host %s:%d%n", componentId, host, port);
                    exception.printStackTrace();
                }
                outcome = Outcome.IOError;
            }
        }

        return outcome;
    }
}
